package it.tukano.reflection;

import it.tukano.fun.Function1;
import it.tukano.fun.VFunction1;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;

/**
 * Extension of a reflective method
 * @author pgi
 */
public class MethodExt implements Runnable, ActionListener {

    /**
     * Instance creator
     * @param instance the instance
     * @param methodName the name of the public method
     * @param arguments the types of the arguments
     * @return the method or null if creation wasn't possible (se the log for reason)
     */
    public static MethodExt newInstance(Object instance, String methodName, Class<?>...arguments) {
        return newInstance(instance.getClass(), instance, methodName, arguments);
    }

    public static MethodExt newInstance(Class<?> type, Object instance, String methodName, Class<?>... arguments) {
        MethodExt method = null;
        try {
            method = new MethodExt(type, instance, methodName, arguments);
        } catch(RuntimeException ex) {
            Logger.getLogger(MethodExt.class.getName()).log(Level.INFO, ex.getMessage(), ex);
        }
        return method;
    }

    private final Method method;
    private final Object instance;
    private final boolean hasAction;
    private final Class<?>[] arguments;
    private final boolean defined;

    /**
     * Instance initializer
     */
    public MethodExt(Object instance, String methodName, Class<?>... arguments) {
        this(instance.getClass(), instance, methodName, arguments);
    }

    public MethodExt(Class<?> type, Object instance, String methodName, Class<?>... arguments) {
        this.arguments = arguments;
        this.instance = instance;
        hasAction = arguments != null && arguments.length == 1 && arguments[0] == ActionEvent.class;
        boolean isDefined = false;
        Method m = null;
        try {
            m = type.getMethod(methodName, arguments);
            isDefined = true;
        } catch (Exception ex) {
            isDefined = false;
            if(instance != null) {
                Logger.getLogger(MethodExt.class.getName()).log(Level.WARNING, "method {0} doesn''t exist in class {1}", new Object[]{methodName, instance.getClass()});
            }
        }
        defined = isDefined;
        method = m;
    }
    
    /**
     * A method ext is undefined if the construction failed because the requested method didn't exist
     * @return true if the method esists, false otherwise
     */
    public boolean isDefined() {
        return defined;
    }

    /**
     * Returns a Function1 that calls this method. The parameter of the function
     * will be used as argument of this method. The value returned by the reflective
     * call will be cast to the retValue type and returned by the function
     * @param <A> the type of the function's argument
     * @param <R> the type of the function's returns value
     * @param arg the class of the argument
     * @param retValue the class of the returned value
     * @return a Function1 that will invoke this when applied
     */
    public <A, R> Function1<A, R> asFunction1(Class<A> arg, final Class<R> retValue) {
        return new Function1<A, R>() {

            public R apply(A p) {
                return retValue.cast(invoke(p));
            }
        };
    }
    /** Use RefJump1 instead of this (less burden in the source code) */
    @Deprecated
    public <A> Function1<A, Void> vfun(Class<A> arg) {
        return new VFunction1<A>() {

            @Override
            protected void doApply(A p) {
                invoke(p);
            }
        };
    }

    /**
     * Invokes this method. All checked exceptions are wrapped into RuntimeExceptions
     * @param parameters the parameters
     * @return the result of the invocation, if any
     */
    public Object invoke(Object...parameters) {
        try {
            return method.invoke(instance, parameters);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException(ex);
        } catch (InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns this method as an ActionListener. The method must have an empty
     * argument's list or one ActionEvent as argument list.
     * @return an ActionListener that calls this method when invoked
     */
    public ActionListener asActionListener() {
        return this;
    }

    /**
     * Returns this method as a Runnable
     * @return this
     */
    public Runnable asRunnable() {
        return this;
    }

    /**
     * As a runnable, this method can be "run". The method must have an
     * empty argument's list
     */
    public void run() {
        invoke();
    }

    /**
     * Checks if this method has an argument list compatible with the given
     * types list
     * @param types the types list to check. Can be null or empty
     * @return true if the arg list of this method is compatible witht the given
     * type list (that is if this method can be called with the given list of
     * parameter types)
     */
    public boolean hasArgList(Class<?>... types) {
        if(types == null && arguments == null) return true;
        if(types == null && arguments != null) return false;
        if(types != null && arguments == null) return false;
        if(types.length != arguments.length) return false;
        for (int i = 0; i < types.length; i++) {
            Class<?> requested = types[i];
            Class<?> available = arguments[i];
            if(!available.isAssignableFrom(requested)) return false;
        }
        return true;
    }

    /**
     * As an action listener this method ext responds to action performed calls.
     * Throws a runtime exception if this method has not an empty argument list
     * or if it has not a single ActionEvent as argument list.
     * @param e the action event.
     */
    public void actionPerformed(ActionEvent e) {
        if(hasAction) invoke(e); else invoke();
    }

    public void callOnEdt() {
        if (SwingUtilities.isEventDispatchThread()) {
            run();
        } else {
            SwingUtilities.invokeLater(this);
        }
    }
}
